<?php

namespace System\Core;

use System\Lib\Log;
use System\Lib\File;
use \System\Types;

/**
 * Description of Output
 *
 * @author elton.schivei
 */
class Output extends Types\Object
{

    protected $final_output;
    protected $cache_expiration = 0;
    protected $headers = array();
    protected $mime_types = array();
    protected $enable_profiler = false;
    protected $_zlib_oc = false;
    protected $_profiler_sections = array();

    /**
     * Whether or not to parse variables like {elapsed_time} and {memory_usage}
     * @var boolean
     */
    protected $parse_exec_vars = true;
    private static $_instance;

    public static function &GetInstance()
    {
        if (!self::$_instance)
            self::$_instance = new self();

        return self::$_instance;
    }

    function __construct()
    {
        parent::__construct();

        $this->_zlib_oc = @\ini_get('zlib.output_compression');

        $this->mime_types = Config::GetValue(Config::GENERAL, 'mimes');

        Log::Write('Output Class Initialized', Log::DEBUG);
    }

    /**
     * Get Output
     *
     * Returns the current output string
     *
     * @access	public
     * @return	string
     */
    public function GetOutput()
    {
        return $this->final_output;
    }

    // --------------------------------------------------------------------

    /**
     * Set Output
     *
     * Sets the output string
     *
     * @access	public
     * @param	string
     * @return	void
     */
    public function SetOutput($output)
    {
        $this->final_output = $output;

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Append Output
     *
     * Appends data onto the output string
     *
     * @access	public
     * @param	string
     * @return	void
     */
    public function AppendOutput($output)
    {
        if ($this->final_output == '')
        {
            $this->final_output = $output;
        }
        else
        {
            $this->final_output .= $output;
        }

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Set Header
     *
     * Lets you set a server header which will be outputted with the final display.
     *
     * Note:  If a file is cached, headers will not be sent.  We need to figure out
     * how to permit header data to be saved with the cache data...
     *
     * @access	public
     * @param	string
     * @return	void
     */
    public function SetHeader($header, $replace = true)
    {
        // If zlib.output_compression is enabled it will compress the output,
        // but it will not modify the content-length header to compensate for
        // the reduction, causing the browser to hang waiting for more data.
        // We'll just skip content-length in those cases.

        if ($this->_zlib_oc && \strncasecmp($header, 'content-length', 14) == 0)
        {
            return;
        }

        $this->headers[] = array($header, $replace);

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Set Content Type Header
     *
     * @access	public
     * @param	string	extension of the file we're outputting
     * @return	void
     */
    public function SetContentType($mime_type)
    {
        if (\strpos($mime_type, '/') === false)
        {
            $extension = \ltrim($mime_type, '.');

            // Is this extension supported?
            if (isset($this->mime_types[$extension]))
            {
                $mime_type = & $this->mime_types[$extension];

                if (\is_array($mime_type))
                {
                    $mime_type = \current($mime_type);
                }
            }
        }

        $header = 'Content-Type: ' . $mime_type;

        $this->headers[] = array($header, true);

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Set HTTP Status Header
     * moved to Common procedural functions in 1.7.2
     *
     * @access	public
     * @param	int		the status code
     * @param	string
     * @return	void
     */
    public function SetStatusHeader($code = 200, $text = '')
    {
        Engineer::SetStatusHeader($code, $text);

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Enable/disable Profiler
     *
     * @access	public
     * @param	bool
     * @return	void
     */
    public function EnableProfiler($val = true)
    {
        $this->enable_profiler = (\is_bool($val)) ? $val : true;

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Set Profiler Sections
     *
     * Allows override of default / config settings for Profiler section display
     *
     * @access	public
     * @param	array
     * @return	void
     */
    public function SetProfilerSections($sections)
    {
        foreach ($sections as $section => $enable)
        {
            $this->_profiler_sections[$section] = ($enable !== false) ? true : false;
        }

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Set Cache
     *
     * @access	public
     * @param	integer
     * @return	void
     */
    public function Cache($time)
    {
        $this->cache_expiration = (!\is_numeric($time)) ? 0 : $time;

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Display Output
     *
     * All "view" data is automatically put into this variable by the controller class:
     *
     * $this->final_output
     *
     * This function sends the finalized output data to the browser along
     * with any server headers and profile data.  It also stops the
     * benchmark timer so the page rendering speed and memory usage can be shown.
     *
     * @access	public
     * @return	mixed
     */
    public function Display($output = '')
    {
        $BM = Benchmark::GetInstance();

        $CI = null;

        if (\class_exists('Controller'))
            $CI = Controller::GetInstance();

        // --------------------------------------------------------------------
        // Set the output data
        if ($output == '')
        {
            $output = & $this->final_output;
        }

        // --------------------------------------------------------------------
        // Do we need to write a cache file?  Only if the controller does not have its
        // own _output() method and we are not dealing with a cache file, which we
        // can determine by the existence of the $CI object above
        if ($this->cache_expiration > 0 && isset($CI) && !\method_exists($CI, '_output'))
        {
            $this->_write_cache($output);
        }

        // --------------------------------------------------------------------
        // Parse out the elapsed time and memory usage,
        // then swap the pseudo-variables with the data

        $elapsed = $BM->ElapsedTime('total_execution_time_start', 'total_execution_time_end');

        if ($this->parse_exec_vars === true)
        {
            $memory = (!\function_exists('memory_get_usage')) ? '0' : \round(\memory_get_usage() / 1024 / 1024, 2) . 'MB';

            $output = \str_replace('{elapsed_time}', $elapsed, $output);
            $output = \str_replace('{memory_usage}', $memory, $output);
        }

        // --------------------------------------------------------------------
        // Is compression requested?
        if (Config::GetValue(Config::GENERAL, 'compress_output') === true && $this->_zlib_oc == false)
        {
            if (\extension_loaded('zlib'))
            {
                if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) and \strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false)
                {
                    \ob_start('ob_gzhandler');
                }
            }
        }

        // --------------------------------------------------------------------
        // Are there any server headers to send?
        if (\count($this->headers) > 0)
        {
            foreach ($this->headers as $header)
            {
                @\header($header[0], $header[1]);
            }
        }

        // --------------------------------------------------------------------
        // Does the $CI object exist?
        // If not we know we are dealing with a cache file so we'll
        // simply echo out the data and exit.
        if (!isset($CI))
        {
            echo $output;
            Log::Write('Final output sent to browser', Log::DEBUG);
            Log::Write("Total execution time: {$elapsed}", Log::DEBUG);
            return true;
        }

        // --------------------------------------------------------------------
        // Do we need to generate profile data?
        // If so, load the Profile class and run it.
        if ($this->enable_profiler == true)
        {
            if (!empty($this->_profiler_sections))
            {
                Profiler::SetSections($this->_profiler_sections);
            }

            // If the output data contains closing </body> and </html> tags
            // we will remove them and add them back after we insert the profile data
            if (\preg_match("|</body>.*?</html>|is", $output))
            {
                $output = \preg_replace("|</body>.*?</html>|is", '', $output);
                $output .= Profiler::Run();
                $output .= '</body></html>';
            }
            else
            {
                $output .= Profiler::Run();
            }
        }

        // --------------------------------------------------------------------
        // Does the controller contain a function named _output()?
        // If so send the output there.  Otherwise, echo it.
        if (\method_exists($CI, '_output'))
        {
            $CI->_output($output);
        }
        else
        {
            echo $output;  // Send it to the browser!
        }

        Log::Write('Final output sent to browser', Log::DEBUG);
        Log::Write("Total execution time: {$elapsed}", Log::DEBUG);
    }

    // --------------------------------------------------------------------

    /**
     * Write a Cache File
     *
     * @access	public
     * @return	void
     */
    private function _write_cache($output)
    {
        $path = Config::GetValue(Config::GENERAL, 'cache_path');

        $cache_path = ($path == '') ? Applications::GetAppPath() . 'cache/' : $path;

        if (!\is_dir($cache_path) or !File::IsReallyWritable($cache_path))
        {
            Log::Write("Unable to write cache file: {$cache_path}");
            return;
        }

        $uri = Config::GetValue(Config::GENERAL, 'base_url') .
                Config::GetValue(Config::GENERAL, 'index_page') .
                Uri::GetInstance()->UriString();

        $cache_path .= \md5($uri);

        if (!$fp = @\fopen($cache_path, File::FOPEN_WRITE_CREATE_DESTRUCTIVE))
        {
            Log::Write("Unable to write cache file: {$cache_path}");
            return;
        }

        $expire = \time() + ($this->cache_expiration * 60);

        if (\flock($fp, \LOCK_EX))
        {
            \fwrite($fp, $expire . 'TS--->' . $output);
            \flock($fp, \LOCK_UN);
        }
        else
        {
            Log::Write("Unable to secure a file lock for file at: {$cache_path}");
            return;
        }
        \fclose($fp);
        @\chmod($cache_path, File::FILE_WRITE_MODE);

        Log::Write("Cache file written: {$cache_path}", Log::DEBUG);
    }

    // --------------------------------------------------------------------

    /**
     * Update/serve a cached file
     *
     * @access	public
     * @return	void
     */
    public function DisplayCache()
    {
        $cache_path = Config::GetValue(Config::GENERAL, 'cache_path') ? : Applications::GetAppPath() . 'cache/';

        // Build the file path.  The file name is an MD5 hash of the full URI
        $uri = Config::GetValue(Config::GENERAL, 'base_url') .
                Config::GetValue(Config::GENERAL, 'index_page') .
                URI::GetInstance()->uri_string;

        $filepath = $cache_path . \md5($uri);

        if (!@\file_exists($filepath))
        {
            return false;
        }

        if (!$fp = @\fopen($filepath, File::FOPEN_READ))
        {
            return false;
        }

        \flock($fp, \LOCK_SH);

        $cache = '';
        if (\filesize($filepath) > 0)
        {
            $cache = \fread($fp, \filesize($filepath));
        }

        \flock($fp, \LOCK_UN);
        \fclose($fp);

        // Strip out the embedded timestamp
        if (!\preg_match("/(\d+TS--->)/", $cache, $match))
        {
            return false;
        }

        // Has the file expired? If so we'll delete it.
        if (\time() >= \trim(\str_replace('TS--->', '', $match['1'])))
        {
            if (File::IsReallyWritable($cache_path))
            {
                @\unlink($filepath);
                Log::Write('Cache file has expired. File deleted', Log::DEBUG);
                return false;
            }
        }

        // Display the cache
        $this->Display(\str_replace($match['0'], '', $cache));
        Log::Write('Cache file is current. Sending it to browser.', Log::DEBUG);
        return true;
    }

}
